Spaces:
Running
Running
File size: 7,992 Bytes
1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d f8aae10 1c48a5d 2c80af5 1c48a5d 49f9fdf 2c80af5 49f9fdf 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d f8aae10 1c48a5d f8aae10 2c80af5 1c48a5d 2c80af5 1c48a5d f8aae10 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 1c48a5d 2c80af5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | # ============================================================
# 📄 الملف: 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
@st.cache_resource(show_spinner=False)
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
|