Spaces:
Running
Running
| # ============================================================ | |
| # 📄 الملف: app/database.py | |
| # 🎯 الغرض: طبقة قاعدة البيانات الوحيدة — MongoDB. | |
| # | |
| # يُقرأ رابط الاتصال MONGODB_URI من البيئة/الأسرار (لا يُكتب في الكود): | |
| # • محليًا : من ملف .env (مثال: mongodb://localhost:27017) | |
| # • على HF : من أسرار الـ Space (رابط Atlas: mongodb+srv://...) | |
| # | |
| # مجموعتان (collections): | |
| # evaluators : المقيّمون (email فريد). | |
| # ratings : وثيقة لكل كلمة قُيّمت (صح/خطأ) + اسم وإيميل المقيّم. | |
| # | |
| # أمان: عمليات الحذف في الواجهة مقصورة على إيميل المقيّم الحالي، | |
| # فلا يقدر مقيّم على حذف تقييمات غيره. | |
| # ============================================================ | |
| import os | |
| from datetime import datetime | |
| import certifi | |
| import pandas as pd | |
| import streamlit as st | |
| # اسم قاعدة البيانات الافتراضي إن لم يُحدَّد داخل الرابط | |
| DEFAULT_DB_NAME = "reverse_dictionary" | |
| # خرائط الأعمدة → أسماء عربية للعرض في الواجهة | |
| _AR_COLUMNS = { | |
| "created_at": "التاريخ والوقت", | |
| "query": "الاستعلام", | |
| "word_type": "التصنيف", | |
| "model": "النموذج", | |
| "word": "الكلمة", | |
| "similarity": "نسبة التشابه", | |
| "verdict": "التقييم", | |
| "evaluator_name": "المقيّم", | |
| "evaluator_email": "الإيميل", | |
| } | |
| # ------------------------------------------------------------ | |
| # 🔌 قراءة الإعدادات والاتصال | |
| # ------------------------------------------------------------ | |
| def _setting(key: str) -> str | None: | |
| """يقرأ إعدادًا من متغيّرات البيئة أولًا (HF Spaces / .env)، ثم من | |
| st.secrets فقط إن وُجد ملف secrets.toml — لتفادي تحذير «No secrets found».""" | |
| val = os.getenv(key) | |
| if val: | |
| return val | |
| secret_paths = ( | |
| "/root/.streamlit/secrets.toml", | |
| "/app/.streamlit/secrets.toml", | |
| os.path.expanduser("~/.streamlit/secrets.toml"), | |
| ".streamlit/secrets.toml", | |
| ) | |
| if any(os.path.exists(p) for p in secret_paths): | |
| try: | |
| return st.secrets[key] | |
| except Exception: | |
| return None | |
| return None | |
| def is_configured() -> bool: | |
| """هل MONGODB_URI مضبوط؟ (التطبيق يتطلّبه للتخزين).""" | |
| return _setting("MONGODB_URI") is not None | |
| def _get_db(): | |
| """يفتح الاتصال مرة واحدة، وينشئ فهرس الإيميل الفريد. | |
| tlsCAFile يُستخدم تلقائيًا لروابط Atlas (mongodb+srv) ويُتجاهل محليًا.""" | |
| from pymongo import MongoClient | |
| uri = _setting("MONGODB_URI") or "" | |
| # شهادات TLS لازمة لروابط Atlas (mongodb+srv/ssl)، ومضرّة محليًا (localhost) | |
| low = uri.lower() | |
| use_tls = ("mongodb+srv" in low) or ("tls=true" in low) or ("ssl=true" in low) | |
| kwargs = {"tlsCAFile": certifi.where()} if use_tls else {} | |
| client = MongoClient(uri, **kwargs) | |
| try: | |
| db = client.get_default_database() # اسم القاعدة من الرابط إن وُجد | |
| except Exception: | |
| db = None | |
| if db is None: # الرابط بلا اسم قاعدة → الافتراضي | |
| db = client[DEFAULT_DB_NAME] | |
| db["evaluators"].create_index("email", unique=True) | |
| return db | |
| def backend_label() -> str: | |
| """وصف مكان التخزين (يُعرض في الشريط الجانبي).""" | |
| return "MongoDB" | |
| # ------------------------------------------------------------ | |
| # 👤 المقيّمون | |
| # ------------------------------------------------------------ | |
| def upsert_evaluator(name: str, email: str) -> None: | |
| """يُدرج المقيّم أو يحدّث اسمه (المفتاح: الإيميل).""" | |
| _get_db()["evaluators"].update_one( | |
| {"email": email}, | |
| {"$set": {"name": name}, | |
| "$setOnInsert": {"email": email, "created_at": datetime.now()}}, | |
| upsert=True, | |
| ) | |
| # ------------------------------------------------------------ | |
| # 💾 حفظ تقييم استعلام كامل (كل النماذج وكلماتها دفعة واحدة) | |
| # ------------------------------------------------------------ | |
| def save_evaluation(query: str, per_model: list[dict], | |
| evaluator_name: str, evaluator_email: str, | |
| word_type: str = "") -> int: | |
| """per_model = [{"model": str, "items": [{"lemma","score","chosen"}, ...]}, ...]. | |
| word_type = تصنيف الكلمة المختار في الواجهة (فعل / معنى معجمي متقدم / معنى عام). | |
| يرجّع عدد الوثائق المحفوظة.""" | |
| ts = datetime.now() # وقت واحد لكل وثائق هذا الحفظ (لتماسك التجميع) | |
| docs = [] | |
| for block in per_model: | |
| for item in block["items"]: | |
| docs.append({ | |
| "created_at": ts, | |
| "query": query, | |
| "word_type": word_type, | |
| "model": block["model"], | |
| "word": item["lemma"], | |
| "similarity": round(float(item["score"]), 3), | |
| "verdict": "صح" if item["chosen"] else "خطأ", | |
| "evaluator_name": evaluator_name, | |
| "evaluator_email": evaluator_email, | |
| }) | |
| if not docs: | |
| return 0 | |
| _get_db()["ratings"].insert_many(docs) | |
| return len(docs) | |
| # ------------------------------------------------------------ | |
| # 📖 قراءة التقييمات (يمكن قصرها على مقيّم) — ترجع DataFrame بأعمدة عربية | |
| # ------------------------------------------------------------ | |
| def load_ratings(evaluator_email: str | None = None) -> pd.DataFrame: | |
| flt = {"evaluator_email": evaluator_email} if evaluator_email else {} | |
| rows = list(_get_db()["ratings"].find(flt).sort("created_at", 1)) | |
| if not rows: | |
| return pd.DataFrame() | |
| df = pd.DataFrame(rows).drop(columns=["_id"], errors="ignore") | |
| return df.rename(columns=_AR_COLUMNS) | |
| # ------------------------------------------------------------ | |
| # 📊 ملخّص أداء النماذج (نسبة الصح) — يمكن قصره على مقيّم | |
| # ------------------------------------------------------------ | |
| def get_summary(evaluator_email: str | None = None) -> pd.DataFrame: | |
| df = load_ratings(evaluator_email) | |
| if df.empty: | |
| return pd.DataFrame() | |
| summary = df.groupby("النموذج")["التقييم"].value_counts().unstack(fill_value=0) | |
| for col in ["صح", "خطأ"]: | |
| if col not in summary.columns: | |
| summary[col] = 0 | |
| summary["الإجمالي"] = summary["صح"] + summary["خطأ"] | |
| summary["نسبة الصح"] = (summary["صح"] / summary["الإجمالي"] * 100).round(1).astype(str) + "%" | |
| return summary[["صح", "خطأ", "الإجمالي", "نسبة الصح"]] | |
| # ------------------------------------------------------------ | |
| # 🗑️ حذف (مقصور على إيميل المقيّم الحالي للأمان) | |
| # ------------------------------------------------------------ | |
| def delete_query(created_at, query: str, evaluator_email: str) -> int: | |
| """يحذف تقييم استعلام واحد لهذا المقيّم.""" | |
| res = _get_db()["ratings"].delete_many({ | |
| "created_at": created_at, | |
| "query": query, | |
| "evaluator_email": evaluator_email, | |
| }) | |
| return res.deleted_count | |
| def delete_all_for(evaluator_email: str) -> int: | |
| """يحذف كل تقييمات المقيّم الحالي فقط.""" | |
| res = _get_db()["ratings"].delete_many({"evaluator_email": evaluator_email}) | |
| return res.deleted_count | |