|
تُرجمت هذه الوثيقة آليًا من الإنجليزية باستخدام Claude AI. ينبغي مراجعة المصطلحات التقنية الجوية/الأرصادية من قِبل ناطق أصلي باللغة قبل الاستخدام في بيئة الإنتاج. راجع النسخة الإنجليزية الأصلية للاطلاع على المرجع الموثوق. |
1. نظرة عامة على البنية المعمارية
WIS2 Downloader هو نظام موزَّع يتكون من أربعة مكونات رئيسية:
Docker Compose
┌──────────────────────────────────────────────────────────┐
│ │
│ Web UI (NiceGUI :8080) │
│ - reads GDC catalogue data from Redis cache │
│ - posts subscriptions to Subscription Manager API │
│ │ │
│ ▼ │
│ Subscription Manager (Flask API :5001) │
│ - persists subscriptions in Redis │
│ - publishes subscribe/unsubscribe commands via PubSub │
│ │ │
│ Redis PubSub (commands) │
│ │ │
│ ▼ │
│ Subscriber (MQTT Client) │
│ - connects to WIS2 Global Broker │
│ - on notification: queues Celery download task │
│ │ │
│ Redis (Celery queue + dedup state) │
│ │ │
│ ▼ │
│ Celery Workers │
│ - download file, verify hash, apply filters, save │
│ │
└──────────────────────────────────────────────────────────┘
1.1. تدفق البيانات
-
عند بدء التشغيل، تجلب واجهة الويب سجلات WCMP2 من نقاط نهاية GDC الثلاث (أو من ذاكرة التخزين المؤقت في Redis) وتبني تسلسلاً هرمياً للمواضيع في الذاكرة
-
يتصفح المستخدم عرض الكتالوج أو الشجرة ويختار موضوعاً في واجهة الويب
-
ينقر المستخدم على اشتراك ← تُرسل واجهة الويب طلب POST إلى واجهة برمجة تطبيقات Subscription Manager
-
يحفظ Subscription Manager الاشتراك في Redis وينشر أمر اشتراك إلى قناة Redis PubSub
-
يتلقى Subscriber الأمر ويشترك في موضوع MQTT على WIS2 Global Broker
-
عند وصول إشعار WIS2، يضع Subscriber مهمة تنزيل Celery في قائمة الانتظار
-
يقوم عامل Celery بتنزيل الملف، والتحقق من الهاش، وتطبيق المرشحات، وحفظه على القرص
2. بنية الوحدات
modules/
├── shared/ # Shared utilities
│ └── shared/
│ ├── __init__.py
│ ├── logging.py # Centralized logging
│ └── redis_client.py # Redis client singleton
│
├── subscriber/ # MQTT subscriber service
│ └── subscriber/
│ ├── __init__.py
│ ├── manager.py # Entry point, thread management
│ ├── subscriber.py # MQTT client wrapper
│ └── command_listener.py # Redis PubSub listener
│
├── subscription_manager/ # REST API service
│ └── subscription_manager/
│ ├── __init__.py
│ ├── app.py # Flask application
│ └── static/
│ └── openapi.yml # API specification
│
├── task_manager/ # Celery tasks
│ └── task_manager/
│ ├── __init__.py
│ ├── worker.py # Celery app configuration
│ ├── tasks/
│ │ └── wis2.py # Download tasks
│ └── workflows/
│ └── __init__.py # Task chains
│
└── ui/ # NiceGUI web interface (port 8080)
├── main.py # Entry point, page route, AppState
├── layout.py # PageLayout builder
├── config.py # Environment variable config
├── data.py # GDC data layer — fetch, merge, cache, hierarchy
├── assets/
│ └── base.css # All custom CSS
├── components/ # Reusable layout components
├── i18n/ # Internationalisation
│ ├── __init__.py # t(), current_lang(), is_rtl(), LANGUAGES
│ ├── en.py # English strings (source of truth)
│ ├── fr.py # French
│ ├── es.py # Spanish
│ ├── ar.py # Arabic (RTL)
│ ├── zh.py # Chinese
│ └── ru.py # Russian
├── models/
│ └── wcmp2.py # WCMP2Record dataclass
└── views/ # Page views (catalogue, tree, subscriptions, etc.)
3. تفاصيل الوحدات
3.1. الوحدة المشتركة
توفر أدوات مساعدة مشتركة تُستخدم عبر جميع الخدمات.
3.1.1. عميل Redis
from shared import get_redis_client
# الحصول على عميل Redis (نمط Singleton)
redis = get_redis_client()
# الاستخدام كعميل Redis عادي
redis.set('key', 'value')
redis.get('key')
العميل:
-
يتصل مباشرةً بخادم Redis
-
يخزن الاتصال مؤقتاً (نمط Singleton)
-
يتضمن منطق إعادة المحاولة للأخطاء العابرة
3.1.2. التسجيل
from shared import setup_logging
# ضبط مسجّل الجذر (يُستدعى مرةً واحدة عند بدء التشغيل)
setup_logging()
# الحصول على مسجّل خاص بالوحدة
LOGGER = setup_logging(__name__)
LOGGER.info("Message with UTC timestamp")
الميزات:
-
طوابع زمنية بالتوقيت العالمي (UTC) بصيغة ISO 8601
-
قابل للإعداد عبر متغير البيئة
LOG_LEVEL -
يُرسل المخرجات إلى stdout لجمع سجلات Docker
3.2. وحدة Subscriber
تتصل بـ WIS2 Global Broker عبر MQTT وتعالج الإشعارات الواردة.
3.2.1. نقطة الدخول
def run_manager():
# 1. Create MQTT subscriber
mqtt_subscriber = Subscriber(**broker_config)
# 2. Create Redis command listener
redis_listener = CommandListener(
subscriber=mqtt_subscriber,
channel=COMMAND_CHANNEL
)
# 3. Start MQTT in separate thread
mqtt_thread = threading.Thread(target=mqtt_subscriber.start, daemon=True)
mqtt_thread.start()
# 4. Start Redis listener (blocks)
redis_listener.start()
3.2.2. مشترك MQTT
class Subscriber:
def __init__(self, host, port, uid, pwd, protocol, session):
# Configure MQTT client with callbacks
self.client = mqtt.Client(...)
self.client.on_message = self._on_message
def _on_message(self, client, userdata, msg):
# Parse notification, create Celery task
job = {
"topic": msg.topic,
"target": target,
"filters": filters,
"payload": json.loads(msg.payload)
}
workflow = wis2_download(job)
workflow.apply_async()
def subscribe(self, topic, target, filters):
self.client.subscribe(topic, qos=0)
self.active_subscriptions[topic] = {...}
def unsubscribe(self, topic):
self.client.unsubscribe(topic)
del self.active_subscriptions[topic]
3.2.3. مستمع الأوامر
class CommandListener(threading.Thread):
def __init__(self, subscriber, channel):
self.subscriber = subscriber
self.pubsub = get_redis_client().pubsub()
def run(self):
self.pubsub.subscribe(self.channel)
while not self.stop_event.is_set():
message = self.pubsub.get_message()
if message:
self._process_command(message)
def _process_command(self, message):
command = json.loads(message['data'])
if command['action'] == 'subscribe':
self.subscriber.subscribe(
command['topic'],
command['save_path'],
command['filters']
)
elif command['action'] == 'unsubscribe':
self.subscriber.unsubscribe(command['topic'])
3.3. وحدة Subscription Manager
واجهة برمجة تطبيقات REST باستخدام Flask لإدارة الاشتراكات.
3.3.1. تطبيق Flask
@app.post('/subscriptions')
def add_subscription():
data = get_json()
topic = normalise_topic(data.get('topic'))
target = normalise_path(data.get('target', ''))
filters = data.get('filters', {})
# Publish command to subscriber via Redis PubSub
command = {
"action": "subscribe",
"topic": topic,
"save_path": target,
"filters": filters
}
publish_command(command, COMMAND_CHANNEL)
# Persist to Redis for durability
persist_subscription(topic, target, filters)
return jsonify({"status": "accepted", ...}), 202
3.3.2. نقطة نهاية المقاييس
تُخزَّن المقاييس في Redis باستخدام عمليات HINCRBYFLOAT الذرية، مما يجعلها آمنة عبر حاويات عامل Celery المتعددة. تقرأ نقطة النهاية من Redis وتُصيِّر تنسيق نص Prometheus:
@app.route('/metrics')
def expose_metrics():
# Update live gauge before exposing
queue_length = redis_client.llen(CELERY_DEFAULT_QUEUE)
set_gauge('celery_queue_length', {'queue_name': CELERY_DEFAULT_QUEUE}, queue_length)
return Response(generate_prometheus_text(), mimetype="text/plain")
زيادة عداد من أي عامل:
from shared import incr_counter
incr_counter('downloads_total', {'cache': hostname, 'media_type': mime_type})
3.4. وحدة Task Manager
عمال Celery لتنزيل الملفات ومعالجتها.
3.4.1. إعداد Celery
app = Celery('tasks',
broker=CELERY_BROKER_URL,
result_backend=CELERY_RESULT_BACKEND)
# الاكتشاف التلقائي للمهام
app.autodiscover_tasks(['task_manager.tasks', 'task_manager.tasks.wis2'])
3.4.2. مهمة التنزيل
@app.task(bind=True)
@metrics_collector
def download_from_wis2(self, job):
result = {...} # Initialize result dict
# 1. Extract identifiers
message_id = job['payload']['id']
data_id = job['payload']['properties']['data_id']
filehash = job['payload']['properties']['integrity']['value']
# 2. Deduplication check
for key, type in [(message_id, 'by-msg-id'), ...]:
if get_status(key, type) == STATUS_SUCCESS:
result['status'] = STATUS_SKIPPED
return result
# 3. Acquire lock
lock_acquired = redis_client.set(lock_key, 1, nx=True, ex=LOCK_EXPIRE)
if not lock_acquired:
raise self.retry(countdown=10, max_retries=10)
# 4. Download file
response = _pool.request('GET', download_url, ...)
# 5. Verify hash
if hash_expected and hash_base64 != hash_expected:
result['status'] = STATUS_FAILED
return result
# 6. Check media type filter
if not _is_allowed_media_type(file_type, filters):
result['status'] = STATUS_SKIPPED
return result
# 7. Save file
output_path.write_bytes(data)
result['status'] = STATUS_SUCCESS
return result
3.4.3. سلاسل سير العمل
def wis2_download(args):
workflow = download_from_wis2.s(args)
return workflow
3.4.4. المُجدوِل (المهام الدورية)
يتولى تطبيق Celery منفصل (scheduler.py) مهام الصيانة باستخدام Celery Beat. يستخدم قواعد بيانات Redis 2/3 تجنبًا للتداخل مع قوائم انتظار عمال التنزيل (قواعد البيانات 0/1). تُعرَّف المهام الدورية في tasks/scheduled_tasks.py:
| المهمة | الفاصل الزمني | الغرض |
|---|---|---|
|
كل 5 دقائق |
تُعيِّن مقاييس |
|
كل 10 دقائق |
تحذف الملفات الأقدم من |
|
يوميًا |
تنفِّذ |
يُشغَّل المُجدوِل كخدمتَيْن في Docker Compose: celery-scheduler-workers (تُشغِّل المهام) و`celery-beats` (تُشغِّلها وفق الجدول الزمني).
4. وحدة واجهة المستخدم
واجهة المستخدم هي تطبيق ويب NiceGUI يعمل على المنفذ 8080. وهي الواجهة الرئيسية للمستخدم لاكتشاف مجموعات البيانات، وتصفح التسلسل الهرمي لمواضيع WIS2، وإنشاء الاشتراكات.
4.1. بنية الوحدات
modules/ui/
├── main.py # Application entry point, page route, AppState
├── layout.py # PageLayout builder
├── config.py # Environment variable config (SUBSCRIPTION_MANAGER_URL etc.)
├── data.py # GDC data layer — fetch, merge, cache, hierarchy
├── assets/
│ └── base.css # All custom CSS (inside @layer components)
├── components/
│ ├── header.py # WMO banner + toolbar + language selector
│ ├── footer.py # Footer bar
│ ├── navigation_drawer.py # Left nav drawer with view links
│ └── page_body.py # Main content area + right sidebar
├── i18n/ # Internationalisation
│ ├── __init__.py # t(), current_lang(), is_rtl(), LANGUAGES
│ ├── en.py # English strings (source of truth)
│ ├── fr.py # French
│ ├── es.py # Spanish
│ ├── ar.py # Arabic (RTL)
│ ├── zh.py # Chinese
│ └── ru.py # Russian
├── models/
│ └── wcmp2.py # WCMP2Record dataclass (GeoJSON Feature)
└── views/
├── dashboard.py # Grafana iframe embed
├── catalogue.py # Catalogue search + result cards
├── tree.py # Topic hierarchy tree
├── subscriptions.py # Active subscriptions list
├── settings.py # GDC status + refresh trigger
└── shared.py # Shared sidebar logic (on_topics_picked, confirm_subscribe, show_metadata)
4.2. طبقة البيانات (data.py)
طبقة البيانات مسؤولة عن جلب سجلات WCMP2 من كتالوجات الاكتشاف العالمية الثلاثة (GDC) لـ WIS2، ودمجها، وبناء ذاكرتَي تخزين مؤقت على مستوى الوحدة تقرأ منهما جميع طرق العرض.
4.2.1. مصادر GDC
يتم جلب السجلات من ثلاث نقاط نهاية GDC عامة عند بدء التشغيل:
| الاسم المختصر | عنوان URL |
|---|---|
|
|
|
|
|
تُخزَّن استجابات JSON الخام في Redis تحت مفاتيح gdc:cache:CMA و`gdc:cache:DWD` و`gdc:cache:ECCC` مع مهلة انتهاء صلاحية يتحكم فيها GDC_CACHE_TTL_SECONDS (الافتراضي 6 ساعات). Redis اختياري — إذا لم يكن متاحاً، يتم جلب البيانات عبر HTTP عند كل بدء تشغيل.
4.2.2. السجلات المدمجة
بعد تحميل جميع مصادر GDC، تُزيل _build_merged_records() التكرارات في السجلات حسب المعرّف عبر الكتالوجات:
-
السجلات الموجودة في كتالوج واحد فقط تظهر مرة واحدة مع إدخال
source_gdcsواحد. -
السجلات الموجودة في كتالوجات متعددة تُدمج: يسرد
source_gdcsجميع الكتالوجات المساهمة. -
يُضبط
has_discrepancyعلىTrueإذا اختلفت الخصائص أو الهندسة أو الروابط بين الكتالوجات. -
تُوحَّد الروابط عبر الكتالوجات حتى تحافظ بيانات القناة الموجودة في أي GDC على السجل المدمج.
تُخزَّن النتيجة في قائمة _merged_records على مستوى الوحدة وتُعاد بواسطة merged_records().
4.2.3. التسلسل الهرمي للمواضيع
تُكرِّر _build_topic_hierarchy() على _merged_records وتُدرج أول قناة MQTT من نوع cache/ لكل سجل في dict متداخل:
{
"cache": {
"children": {
"a": {
"children": {
"wis2": {
"children": {
"de-dwd": {
"children": {
...
"synop": {
"datasets": [<WCMP2Record>, ...]
}
}
}
}
}
}
}
}
}
}
يمكن أن تحتوي العقدة على "children" و`"datasets"` معاً إذا كانت إحدى القنوات بادئة للأخرى.
يُخزَّن التسلسل الهرمي في _topic_hierarchy ويُعاد بواسطة topic_hierarchy(). يستهلكه tree.py لبناء أداة ui.tree، وتستخدمه get_datasets_for_channel() لحل مجموعات البيانات لموضوع معين.
4.2.4. الدوال الرئيسية
| الدالة | الوصف |
|---|---|
|
تُعيد قائمة السجلات المدمجة المخزنة مؤقتاً |
|
تُعيد التسلسل الهرمي للمواضيع المخزن مؤقتاً |
|
تزيل |
|
تجلب بيانات GDC (ذاكرة التخزين المؤقت في Redis أولاً، ثم HTTP)، وتُعيد بناء |
4.3. نموذج WCMP2 (models/wcmp2.py)
WCMP2Record هو تمثيل dataclass لميزة GeoJSON الخاصة ببيانات اكتشاف WIS2.
from models.wcmp2 import WCMP2Record
rec = WCMP2Record.from_dict(feature_dict)
rec.id # str - unique dataset identifier
rec.title # str | None
rec.description # str | None
rec.keywords # list[str]
rec.wmo_data_policy # 'core' | 'recommended' | None
rec.geometry # Geometry | None (.type, .coordinates)
rec.links # list[Link] (.channel, .href, .rel, .extra)
rec.mqtt_channels # list[str] - channels from all links
Link.extra هو dict يلتقط المفاتيح غير الموجودة في المخطط من استجابة GDC (مثل filters الخاصة بـ GDC المستخدمة لحقول المرشح المخصصة في الشريط الجانبي للاشتراك).
4.4. AppState
كل جلسة متصفح لها نسخة AppState خاصة بها (محددة في main.py):
class AppState:
def __init__(self):
self.selected_topics: list[str] = []
self.current_view: str = 'dashboard'
يحمل selected_topics مواضيع MQTT المحددة حالياً في واجهة المستخدم ويقود الشريط الجانبي الأيمن. يتتبع current_view اسم العرض النشط ويُحفظ في app.storage.user['current_view'] قبل أي إعادة تحميل للصفحة (مثلاً عند تغيير اللغة) حتى يُعاد المستخدم إلى نفس العرض.
4.5. طرق العرض
تتبع جميع طرق العرض نفس الاصطلاح: دالة render(container, …) تبني عناصر NiceGUI داخل عنصر الحاوية المقدَّم. render دالة عادية (غير متزامنة) دائماً حتى يمكن استدعاؤها بشكل متزامن من show_view() في main.py.
4.5.1. عرض الكتالوج
يوفر views/catalogue.py بحثاً نصياً كاملاً عبر merged_records() مع دوال مرشح نقية:
| الدالة | الوصف |
|---|---|
|
تطابق الاستعلام مع المعرّف والعنوان والوصف والكلمات المفتاحية ومفاهيم الموضوع |
|
تطابق |
|
كلمات مفتاحية مفصولة بفاصلات؛ يجب أن تظهر كلها في السجل |
|
تقاطع هندسة Shapely مع منطقة الإحداثيات المقدَّمة |
تُصيَّر بطاقات النتائج بواسطة update_search_results()، وهي أيضاً مسؤولة عن الترقيم.
4.5.2. عرض الشجرة
يحوِّل views/tree.py ناتج topic_hierarchy() إلى عقد ui.tree عبر _to_tree_nodes() (متكرر، مرتب أبجدياً). يستخدم التحديد on_select لفرض التحديد الفردي للعقدة — لا يُستخدم on_tick لأنه يتعارض مع مزامنة الحالة الداخلية لـ NiceGUI.
4.5.3. الشريط الجانبي المشترك (views/shared.py)
on_topics_picked(e, state, layout, is_page_selection, dataset_id) هو المعالج المركزي لتحديد الكتالوج والشجرة. يقوم بـ:
-
تحديث
state.selected_topics -
بناء الشريط الجانبي الأيمن بحقل إدخال دليل الحفظ، ومرشحات مجموعات البيانات/نوع الوسائط/منطقة الإحداثيات/التاريخ، والمرشحات المخصصة الاختيارية
-
ربط زر اشتراك بـ
confirm_subscribe()
يعرض confirm_subscribe() نافذة حوار بحمولة JSON الكاملة قبل استدعاء subscribe_to_topics().
يبحث show_metadata(dataset_id) عن السجل من merged_records() ويُصيِّر نافذة حوار التفاصيل مع خريطة Leaflet تفاعلية.
4.6. إضافة عرض جديد
-
أنشئ
modules/ui/views/myview.pyمع دالةrender(container)، باستخدامt()لجميع النصوص المرئية للمستخدم -
أضف إدخالاً للتنقل في
components/navigation_drawer.py— قائمةNAV_ITEMSتأخذ tuple من النوع(view_id, label_key, icon)حيثlabel_keyهو مفتاح i18n من نوعnav.* -
أضف فرعاً في مُوزِّع
show_view()فيmain.py -
أضف أي فئات CSS جديدة إلى
assets/base.cssداخل كتلة@layer components { … } -
أضف جميع مفاتيح النصوص الجديدة إلى
i18n/en.pyوإلى كل ملف لغة آخر — انظر التدويل (i18n)
4.7. التدويل (i18n)
تدعم واجهة المستخدم لغات واجهة متعددة عبر نظام i18n خفيف الوزن مبني على القواميس في modules/ui/i18n/.
4.7.1. اللغات المدعومة
| الرمز | اللغة | الاتجاه |
|---|---|---|
|
English |
LTR |
|
Français |
LTR |
|
Español |
LTR |
|
العربية |
RTL |
|
中文 |
LTR |
|
Русский |
LTR |
تُخزَّن اللغة النشطة لكل جلسة متصفح في app.storage.user['lang'] وتكون الإنجليزية افتراضياً. يغيِّر المستخدمون اللغة عبر المحدد في شريط أدوات الرأس؛ تُعاد تحميل الصفحة لتطبيق التغيير.
4.7.2. آلية عمل t()
from i18n import t
# بحث بسيط
label = t('nav.dashboard') # → 'Dashboard'
# مع إدراج القيم (يستخدم str.format)
label = t('subscriptions.folder', path='./') # → 'Folder: ./'
سلسلة البحث هي:
-
ملف اللغة المختارة
-
الإنجليزية (
en.py) — احتياطي إذا كان المفتاح غير موجود في اللغة المختارة -
نص المفتاح نفسه — احتياطي إذا كان غير موجود في الإنجليزية أيضاً (مرئي أثناء التطوير)
تقرأ t() قيمة app.storage.user['lang'] من جلسة طلب NiceGUI الحالية. استدعِها دائماً في وقت التصيير (داخل معالج @ui.page أو رد نداء حدث NiceGUI)، وليس في وقت استيراد الوحدة.
|
النصوص التي تحتوي على أحرف { أو } حرفية (مثل أمثلة JSON في نص التلميح) يجب أن تستخدم أقواساً منفردة كما هي. لا تستخدم هروب {{ / }} — تستدعي t() .format(**kwargs) فقط عند تمرير وسيطات الكلمات المفتاحية، لذا سيُعرض {{ حرفياً.
|
4.7.3. اصطلاحات تسمية المفاتيح
تستخدم المفاتيح فضاءات أسماء مفصولة بنقاط:
| البادئة | النطاق | مثال |
|---|---|---|
|
تسميات درج التنقل |
|
|
تسميات الأزرار |
|
|
الشريط الجانبي للاشتراك |
|
|
عرض الكتالوج |
|
|
عرض الشجرة |
|
|
عرض إدارة الاشتراكات |
|
|
عرض الإعدادات |
|
|
عرض الاشتراك اليدوي |
|
|
رسائل التحقق للاشتراك اليدوي |
|
|
عناوين النوافذ الحوارية |
|
|
نافذة حوار البيانات الوصفية |
|
|
رسائل التحقق المشتركة |
|
|
التذييل |
|
|
تسميات إمكانية الوصول ARIA |
|
4.7.4. دعم الكتابة من اليمين إلى اليسار
العربية (ar) تُكتب من اليمين إلى اليسار. عند اختيار العربية، تُعيد is_rtl() القيمة True ويُضيف معالج on_connect() في main.py الخاصية dir="rtl" على العنصر <html> عبر JavaScript، مما يُفعِّل وضع RTL في تخطيط Quasar. تصحح تجاوزات CSS المخصصة في assets/base.css (تحت تعليق كتلة RTL overrides) الحشو المادي لـ Quasar على .q-page-container وتُعيد تحديد موضع الشريط الجانبي للاشتراك.
لإضافة لغة RTL أخرى، أضف رمزها إلى RTL_LANGUAGES في init.py — لا حاجة لأي تغييرات أخرى.
4.7.5. إضافة لغة جديدة
انسخ modules/ui/i18n/en.py إلى modules/ui/i18n/{code}.py وترجم جميع القيم. احتفظ بكل مفتاح؛ لا تحذف أياً منها. احتفظ بأسماء {placeholder} تماماً كما تظهر في المصدر الإنجليزي:
# modules/ui/i18n/de.py
"""German strings."""
STRINGS: dict[str, str] = {
'nav.dashboard': 'Dashboard',
'nav.catalogue': 'Katalogsuche',
'nav.tree': 'Baumsuche',
'nav.manual': 'Manuell abonnieren',
'nav.manage': 'Abonnements verwalten',
'nav.settings': 'Einstellungen',
# ... all remaining keys ...
'subscriptions.folder': 'Ordner: {path}', # {path} must be preserved
'settings.records': '{name}: {count} Einträge',
}
init.pyfrom . import ar, de, en, es, fr, ru, zh (1)
LANGUAGES: dict[str, str] = {
'en': 'English',
'fr': 'Français',
'es': 'Español',
'ar': 'العربية',
'zh': '中文',
'ru': 'Русский',
'de': 'Deutsch', (2)
}
_STRINGS: dict[str, dict[str, str]] = {
'en': en.STRINGS,
'fr': fr.STRINGS,
'es': es.STRINGS,
'ar': ar.STRINGS,
'zh': zh.STRINGS,
'ru': ru.STRINGS,
'de': de.STRINGS, (3)
}
| 1 | أضف الاستيراد |
| 2 | أضف الاسم المعروض (بالخط الأصلي) |
| 3 | أضف تعيين النصوص |
إذا كانت اللغة تُكتب من اليمين إلى اليسار (مثل العبرية he، الفارسية fa):
RTL_LANGUAGES: frozenset[str] = frozenset({'ar', 'he'})
docker-compose restart wis2downloader-ui
ستظهر اللغة الجديدة فوراً في محدد اللغة في الرأس.
| الترجمات غير الإنجليزية الحالية مُنشأة آلياً وتُقدَّم كنقطة بداية فحسب. ينبغي مراجعة جميع النصوص من قِبل ناطق أصلي قبل النشر في بيئة الإنتاج، مع إيلاء اهتمام خاص لمصطلحات مجال WMO/الأرصاد الجوية (WIS2, BUFR, GRIB, Global Cache، إلخ) التي لها ترجمات راسخة في الوثائق الرسمية لـ WMO. |
4.7.6. إضافة نصوص جديدة قابلة للترجمة
عند إضافة نص واجهة مستخدم إلى أي عرض أو مكوّن:
en.py (مصدر الحقيقة)# modules/ui/i18n/en.py
'myview.title': 'My New View',
'myview.description': 'Showing results for {topic}.',
انسخ القيمة الإنجليزية كعنصر نائب إذا لم تكن الترجمة متاحة بعد. تعود t() إلى الإنجليزية تلقائياً، لكن وجود المفتاح يتجنب الفجوات في الأدوات:
# fr.py / es.py / ar.py / zh.py / ru.py
'myview.title': 'My New View', # TODO: translate
'myview.description': 'Showing results for {topic}.',
t() في العرضfrom i18n import t
def render(container):
with container:
ui.label(t('myview.title')).classes('page-title')
ui.label(t('myview.description', topic=selected_topic))
5. التواصل بين الخدمات
5.1. Redis PubSub (الأوامر)
يستخدم التواصل بين Subscription Manager وSubscriber نظام Redis PubSub على قناة subscription_commands:
// First subscription for a topic — opens MQTT connection
{"action": "subscribe", "topic": "cache/a/wis2/+/data/#",
"subscriptions": {"<uuid>": {"id": "<uuid>", "save_path": "all-data", "filter": {}}}}
// Additional subscription for an already-open topic
{"action": "add_subscription", "topic": "cache/a/wis2/+/data/#",
"sub_id": "<uuid>", "save_path": "grib-data", "filter": {"rules": [...]}}
// Remove one subscription (MQTT stays open)
{"action": "remove_subscription", "topic": "cache/a/wis2/+/data/#", "sub_id": "<uuid>"}
// Last subscription removed — closes MQTT connection
{"action": "unsubscribe", "topic": "cache/a/wis2/+/data/#"}
// Update save_path or filter for an existing subscription
{"action": "update_subscription", "topic": "cache/a/wis2/+/data/#",
"sub_id": "<uuid>", "save_path": "new-path", "filter": {}}
5.2. مهام Celery (التنزيلات)
يستخدم التواصل بين Subscriber وعمال Celery قائمة انتظار مهام Celery:
job = {
"topic": "cache/a/wis2/de-dwd/data/...",
"target": "dwd-data",
"filter": {"name": "...", "rules": [...]}, # subscription-level filter
"_broker": "globalbroker.meteo.fr",
"_received": "2026-01-28 10:15:30",
"_queued": "2026-01-28 10:15:30",
"payload": {
"id": "...",
"properties": {
"data_id": "...",
"metadata_id": "...",
"integrity": {"method": "sha512", "value": "..."}
},
"links": [
{"rel": "canonical", "href": "https://..."}
]
}
}
5.3. مفاتيح Redis
| نمط المفتاح | الغرض |
|---|---|
|
Hash لجميع الاشتراكات (sub_id ← JSON |
|
تتبع إزالة التكرار (الأنواع: |
|
قفل موزَّع للتنزيلات المتزامنة |
|
JSON كتالوج GDC المخزَّن مؤقتاً (CMA، DWD، ECCC)؛ مدة الصلاحية تُحدَّد بـ |
|
Hash مقاييس Prometheus (الحقل = قاموس labels بتنسيق JSON، القيمة = عداد/مقياس من النوع float) |
|
نبضة صحة المشترك |
|
قائمة انتظار مهام Celery (الافتراضية) |
6. توسيع النظام
6.1. إضافة شرط مطابقة جديد لمحرك الفلتر
يقع محرك الفلتر في modules/shared/shared/filters.py. كل شرط مطابقة هو مفتاح في كائن المطابقة تُرسله _evaluate_match().
لإضافة شرط مدمج جديد (مثلاً المطابقة على حقل بيانات وصفية جديد station_id):
-
أضف الحقل إلى
MatchContextفيfilters.py:@dataclass class MatchContext: ... station_id: str | None = None -
امْلأه في
_build_context()فيwis2.py(قبل التنزيل و/أو بعده). -
أضف فرعاً في
_evaluate_match():if 'station_id' in match: return _match_string_field(ctx.station_id, match['station_id']) -
وثِّق الحقل الجديد في
openapi.ymlوفي مرجع الفلتر في دليل المستخدم.
لا يلزم إجراء أي تغييرات في app.py أو command_listener.py أو subscriber.py — يُمرَّر كائن الفلتر دون تعديل ويُقيَّم عند وقت التنزيل.
6.2. إضافة مهمة جديدة
-
إنشاء المهمة في
modules/task_manager/task_manager/tasks/ -
التسجيل في
worker.pyللاكتشاف التلقائي -
إضافتها إلى سير العمل إذا لزم الأمر
# modules/task_manager/task_manager/tasks/custom.py
from task_manager.worker import app
@app.task
def my_custom_task(data):
# Process data
return result
6.3. إضافة مقياس جديد
تُخزَّن المقاييس في Redis عبر shared.redis_metrics. لإضافة مقياس جديد:
-
سجِّله في قاموس
METRICSفيmodules/shared/shared/redis_metrics.py:# short name → (prometheus type, help text) METRICS: dict[str, tuple[str, str]] = { ... 'my_counter_total': ('counter', 'Description of the counter.'), 'my_gauge': ('gauge', 'Description of the gauge.'), } -
زِد قيمته من أي خدمة (عداد) أو اضبطها (مقياس):
from shared import incr_counter, set_gauge # عداد (مثلاً في مهمة Celery) incr_counter('my_counter_total', {'label1': 'value', 'label2': 'value'}) # مقياس (مثلاً في مهمة مجدوَلة) set_gauge('my_gauge', {'label1': 'value'}, 42.0)
7. الاختبار
7.1. تشغيل الاختبارات
# تثبيت تبعيات الاختبار
pip install pytest pytest-cov
# تشغيل الاختبارات
pytest modules/
# مع تقرير التغطية
pytest --cov=modules modules/
7.2. الاختبار اليدوي
# تشغيل الخدمات
docker-compose up -d
# إنشاء اشتراك تجريبي
curl -X POST http://localhost:5002/subscriptions \
-H "Content-Type: application/json" \
-d '{"topic": "cache/a/wis2/de-dwd/data/#", "target": "test"}'
# مراقبة السجلات
docker-compose logs -f subscriber celery
# فحص التنزيلات
ls -la downloads/test/
8. إعداد بيئة التطوير
8.1. التطوير المحلي
# إنشاء بيئة افتراضية
python -m venv venv
source venv/bin/activate
# تثبيت الوحدات في الوضع القابل للتعديل
pip install -e modules/shared
pip install -e modules/task_manager
pip install -e modules/subscriber
pip install -e modules/subscription_manager
# تشغيل Redis (نسخة واحدة للتطوير)
docker run -d -p 6379:6379 redis:7.2-alpine redis-server --requirepass devpassword
# ضبط البيئة
export REDIS_HOST=localhost
export REDIS_PORT=6379
export REDIS_PASSWORD=devpassword
export FLASK_SECRET_KEY=dev-secret-key
export LOG_LEVEL=DEBUG
# تشغيل مدير الاشتراكات
python -m subscription_manager.app
# تشغيل المشترك (في طرفية أخرى)
export GLOBAL_BROKER_HOST=globalbroker.meteo.fr
python -m subscriber.manager
# تشغيل عامل Celery (في طرفية أخرى)
celery -A task_manager.worker worker --loglevel=DEBUG
8.2. بناء Docker
# بناء جميع الصور
docker-compose build
# بناء خدمة محددة
docker-compose build celery
# إعادة البناء دون ذاكرة مؤقتة
docker-compose build --no-cache
9. التنقيح (Debugging)
9.1. عرض مهام Celery
# فحص المهام النشطة
docker exec -it wis2downloader-celery-1 \
celery -A task_manager.worker inspect active
# فحص المهام المحجوزة
docker exec -it wis2downloader-celery-1 \
celery -A task_manager.worker inspect reserved
9.2. فحص Redis
# الاتصال بـ Redis (استخدم كلمة المرور من البيئة)
docker exec -it redis redis-cli -a $REDIS_PASSWORD
# سرد الاشتراكات
HGETALL global:subscriptions
# فحص طول قائمة الانتظار
LLEN celery
# عرض مفاتيح إزالة التكرار
KEYS wis2:notifications:*
# عرض ذاكرة كتالوج GDC المؤقتة (تُملأ بواجهة المستخدم عند بدء التشغيل)
KEYS gdc:cache:*
TTL gdc:cache:CMA
تتطلب جميع أوامر Redis المصادقة. علامة -a $REDIS_PASSWORD تمرر كلمة المرور من بيئتك.
|
9.3. تنقيح MQTT
# الاشتراك في موضوع يدويًا (mosquitto-clients)
mosquitto_sub -h globalbroker.meteo.fr -p 443 \
-t 'cache/a/wis2/de-dwd/data/#' \
-u everyone -P everyone \
--capath /etc/ssl/certs